package deepr.filter;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import deepr.core.*;
import deepr.src.graph.GraphFilter;
import deepr.src.pg.PgFilter;
import deepr.anno.SchemaType;
import deepr.anno.Type;
import deepr.lib.CastLib;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;

import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.util.*;

@Slf4j
@Data
public class FilterParser {

  private DisposeContext ctx;
  private boolean meetOr = false;
  private Object orValue;
  private SchemaType schemaType;
  private Gson gson = new GsonBuilder().disableHtmlEscaping().create();
  private HashMap<String, Integer> appearCount = new HashMap<>();
  private DbFilterBehavior dbFilterBehaviour;
  private int depth = 1;

  HashMap<String, Object> filterParams = new HashMap<>();
  LinkedList<StringBuilder> graphMatch = new LinkedList<>();

  private String handlerJsonKey = null;
  private Object handlerJsonValue = null;


  private String joiner = " AND ";

  public FilterParser(DisposeContext ctx, DbFilterBehavior filterBehavior) {
    setSchemaType(ctx.getSchemaType());
    this.dbFilterBehaviour = filterBehavior;

  }

  public FilterParser(DisposeContext ctx) {
    setSchemaType(ctx.getSchemaType());
    switch (ctx.getSchemaType()) {
      case PG:
        dbFilterBehaviour = new PgFilter();
        break;
      case NEO4J:
        dbFilterBehaviour = new GraphFilter();
        break;
      default:
        Assert.isTrue(false, "Not Set SchemaType");
        break;
    }
  }

  public SchemaType getSchemaType() {
    return ctx.getSchemaType();
  }

  public void setDisposeContext(DisposeContext ctx) {
    this.ctx = ctx;
  }

  public DisposeContext getDisposeContext() {
    return this.ctx;
  }


  // [
  //  {"ts,between":[t1, t2]},
  //  {"name,like":"name"},
  //  {"recommend,>":5},
  //  {"or":[{"status": [1,2]},{"v2":[1,2,3]},{"device.id,>":5},{"id,<":2},{"device":{}}, {"collection.user.id":[]]},  device: json查询,外表关联查询
  //  {"device.name":"device_name"},
  //  {"device.id":"device_id"},
  //  {"ts,to_date":"2023-01-01 12:29:21"},
  //  {"device":{"name":xxx}}  // jsonb 查询,外键查询
  //  {"id": xxx}
  // ]
  public StringBuilder parse(String object, List<HashMap<String, Object>> filters) throws InvocationTargetException, NoSuchMethodException, InstantiationException, ParseException, IllegalAccessException {
    long t1 = System.currentTimeMillis();
    log.info("filters: {}", gson.toJson(filters));
    iteratorFilter(null, object, filters);
    StringBuilder str = dbFilterBehaviour.getForeignFilter(this);
    HashMap<Integer, HashMap<String, FilterRelay>> depthObjectFilter = dbFilterBehaviour.getDepthObjectFilter();
    if (orValue != null) {
      this.joiner = " OR ";
      dbFilterBehaviour.resetDepthObjectFilter();
      iteratorFilter(null, object, orValue);
      str.append(" AND ").append(dbFilterBehaviour.getForeignFilter(this));
    }
    long tend = System.currentTimeMillis();
    log.info("parse use ts:{}", tend - t1);
    log.info("depth: {}, foreFilterMap: {}, depth:{}", depth, gson.toJson(depthObjectFilter), depthObjectFilter.size());
    log.info("str: {}", str.toString());
    log.info("filterParams:{}", gson.toJson(filterParams));
    return str;
  }

  public StringBuilder parse(String object, Object[] filters) throws InvocationTargetException, NoSuchMethodException, InstantiationException, ParseException, IllegalAccessException {
    long t1 = System.currentTimeMillis();
    log.info("filters: {}", gson.toJson(filters));
    iteratorFilter(null, object, filters);
    StringBuilder str = dbFilterBehaviour.getForeignFilter(this);
    HashMap<Integer, HashMap<String, FilterRelay>> depthObjectFilter = dbFilterBehaviour.getDepthObjectFilter();
    if (orValue != null) {
      this.joiner = " OR ";
      dbFilterBehaviour.resetDepthObjectFilter();
      iteratorFilter(null, object, orValue);
      str.append(" AND ").append(dbFilterBehaviour.getForeignFilter(this));
    }
    long tend = System.currentTimeMillis();
    log.info("parse use ts:{}", tend - t1);
    log.info("depth: {}, foreFilterMap: {}, depth:{}", depth, gson.toJson(depthObjectFilter), depthObjectFilter.size());
    log.info("str: {}", str.toString());
    log.info("filterParams:{}", gson.toJson(filterParams));
    return str;
  }

  public void doFilter(String parent, String object, String keyStr, Object value) throws NoSuchMethodException, InstantiationException, IllegalAccessException, ParseException, InvocationTargetException {
    log.info("parent: {}, object:{}, keyStr:{},value:{}", parent, object, keyStr, value);
    SchemaData schemaData = SchemaManager.getSchemaData(object);
    if (keyStr.equals("or")) {
      Assert.isTrue(!meetOr, "or action only appear once");
      Assert.isTrue(value instanceof HashMap || value.getClass().isArray() || value instanceof LinkedList, "or value must be json or array");
      this.orValue = value;
      meetOr = true;
      return;
    }
    String[] actionInfo = keyStr.split(",");
    int len = actionInfo.length;
    Assert.isTrue(0 < len && len < 3, keyStr + " len invalid");
    // {"device.id,>":5}
    // key = device.id
    String key = actionInfo[0];
    String action = "=";
    if (len > 1) {
      action = actionInfo[1];
    }
    String[] key2 = key.split("[.]");
    int key2Len = key2.length;
    Assert.isTrue(0 < key2Len && key2Len < 6, "key len must in [1,5]");
    PropertyData propertyData;
    StringBuilder sb;
    switch (key2Len) {
      // {"device":{}}/{"device":[]}/{"id":xxx}, json,字段,外表
      case 1:
        String key3 = key2[0];
        propertyData = schemaData.getPropertyData(key3);
        if (propertyData.isSchema()) {
          if (depth >= 2) {
            String forProperty = propertyData.getForProperty();
            dbFilterBehaviour.appendProperty(depth, object, object, forProperty, null, joiner);
          }
          depth++;
          iteratorFilter(object, key3, value);
          depth--;
          return;
        }
        parseKeyLen1Filter(parent, propertyData, object, key3, value, action);
        break;
      // device.director.id, device.area.name 外表json,外表的外表
      // assetCategory.location.record2.user.name,like
      default:
        String realObject = key2[0];
        propertyData = schemaData.getPropertyData(realObject);
        if (propertyData.isSchema()) {
          if (depth >= 2) {
            String forProperty = propertyData.getForProperty();
            dbFilterBehaviour.appendProperty(depth, parent, object, forProperty, null, joiner);
          }
          depth++;
          doFilter(object, realObject, convertToKey(key2, action), value);
          depth--;
          return;
        }
        Type type = propertyData.getType();
        sb = dbFilterBehaviour.jsonKey(this, object, propertyData, realObject, key2, value, action);
        if (type == Type.JsonArray) {
          dbFilterBehaviour.appendJsonArray(depth, parent, object, realObject, sb, joiner);
          return;
        }
        if (type == Type.JSON) {
          dbFilterBehaviour.appendProperty(depth, parent, object, realObject, sb, joiner);
          return;
        }
        break;
    }
  }

  // {"device":{"id":id,"name":name}}
  public void parseKeyLen1Filter(String parent, PropertyData propertyData, String object,
                                 String key, Object value, String action) throws
      InvocationTargetException, NoSuchMethodException, InstantiationException, ParseException, IllegalAccessException {
    log.info("parent:{},object:{},key:{}, value:{}", parent, object, key, value);
    StringBuilder sb;
    Type type = propertyData.getType();
    String key2 = getParamKey(key);
    if (parent != null) {
      key2 = getParamKey(object + "_" + key);
    }
    if (action.equals("like")) {
      sb = dbFilterBehaviour.like(this, object, key, key2, value);
      dbFilterBehaviour.appendProperty(depth, parent, object, key, sb, joiner);
      return;
    }
    boolean valueIsArray = value.getClass().isArray();
    value = handleKeyValue(propertyData, key, value);
    if (type == Type.JSON || type == Type.JsonArray) {
      sb = dbFilterBehaviour.jsonAt(this, object, key, key2, value);
    } else if (action.equals("between")) {
      sb = dbFilterBehaviour.between(this, object, key, key, value);
    } else if (valueIsArray && type == Type.ARRAY) {
      sb = dbFilterBehaviour.twoArray(this, object, key, key2, value);
    } else if (valueIsArray) {
      sb = dbFilterBehaviour.valueArray(this, object, key, key2, value);
    } else if (type == Type.ARRAY) {
      sb = dbFilterBehaviour.propertyArray(this, object, key, key2, value);
    } else
      sb = dbFilterBehaviour.restAction(this, object, key, key2, value, action);
    dbFilterBehaviour.appendProperty(depth, parent, object, key, sb, joiner);
  }

  public String getParamKey(String property) {
    Integer count = appearCount.getOrDefault(property, 0);
    if (count == 0) {
      appearCount.put(property, 1);
      return property;
    }
    appearCount.put(property, count + 1);
    return property + count;
  }

  public Object[] betweenActionValueValid(String key, Object value) {
    Assert.isTrue(value.getClass().isArray(), key + "between value must be array");
    Object[] v = (Object[]) value;
    Assert.isTrue(v.length == 2, key + "between value length must be 2");
    return v;
  }

  public Object handleKeyValue(PropertyData propertyData, String key, Object value) throws
      NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ParseException {
    Class<?> handler = propertyData.getHandler();
    if (handler != null) {
      return handler.getMethod("filter",
          FilterParser.class, String.class, Object.class, Object.class).
          invoke(handler.newInstance(), this, key, value, propertyData.getHandlerArgs());
    }
    Type type = propertyData.getType();
    if (type == Type.INTEGER) {
      return CastLib.toInteger(key, value);
    }
    if (type == Type.LONG) {
      return CastLib.toInteger(key, value);
    }
    if (type == Type.DateTime) {
      return CastLib.toDateTime(key, value);
    }
    if (type == Type.DATE) {
      return CastLib.toDate(key, value);
    }
    return value;
  }


  public Object handleJsonKeyValue(PropertyData propertyData, String key, Object value) throws
      NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, ParseException {
    Class<?> handler = propertyData.getHandler();
    if (handler != null) {
      setHandlerJsonKey(null);
      return handler.getMethod("filter",
          FilterParser.class, String.class, Object.class, Object.class).
          invoke(handler.newInstance(), this, key, value, propertyData.getHandlerArgs());
    }
    return value;
  }


  public void iteratorFilter(String parent, String object, HashMap<String, Object> filters) throws
      InvocationTargetException, NoSuchMethodException, InstantiationException, ParseException, IllegalAccessException {
    for (Map.Entry<String, Object> filterEntry : filters.entrySet()) {
      doFilter(parent, object, filterEntry.getKey(), filterEntry.getValue());
    }
  }

  public void iteratorFilter(String parent, String object, List<HashMap<String, Object>> filters) throws
      NoSuchMethodException, InstantiationException, IllegalAccessException, ParseException, InvocationTargetException {
    for (HashMap<String, Object> filter : filters) {
      iteratorFilter(parent, object, filter);
    }
  }

  public void iteratorFilter(String parent, String object, Object[] filters) throws
      NoSuchMethodException, InstantiationException, IllegalAccessException, ParseException, InvocationTargetException {
    for (Object filter : filters) {
      iteratorFilter(parent, object, filter);
    }
  }

  public void iteratorFilter(String parent, String object, Object value) throws
      NoSuchMethodException, InstantiationException, IllegalAccessException, ParseException, InvocationTargetException {
    if (value instanceof HashMap) {
      iteratorFilter(parent, object, (HashMap<String, Object>) value);
      return;
    }
    if (value.getClass().isArray()) {
      iteratorFilter(parent, object, (Object[]) value);
      return;
    }
    if (value instanceof LinkedList) {
      iteratorFilter(parent, object, (LinkedList) value);
      return;
    }
    Assert.isTrue(false, object + " value type must be json or array");
  }


  public String convertToKey(String[] keys, String action) {
    StringBuilder sb = new StringBuilder();
    for (int i = 1; i < keys.length; i++) {
      if (i > 1)
        sb.append(".");
      sb.append(keys[i]);
    }
    if (action.equals("=")) {
      return sb.toString();
    }
    return sb.append(",").append(action).toString();
  }

}
